package com.simpou.commons.utils.interceptor;

import com.simpou.commons.utils.reflection.Casts;
import com.simpou.commons.utils.reflection.Reflections;
import static com.simpou.commons.utils.validation.Assertions.*;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
 * Responsável por construir o ambiente de interceptação e gerar objetos
 * interceptados.
 *
 * @author Jonas Pereira
 * @since 2011-07-27
 * @version 2012-07-28
 */
public final class InterceptorInvocationHandler implements InvocationHandler {
    /**
     * Interceptador em uso.
     */
    private final Interceptor interceptor;

    /**
     * Objeto cujos métodos serão interceptados.
     */
    private final Object intercepted;

    /**
     * Cache armazena se método deve sofrer interceptação.
     */
    private final Map<Method, Boolean> mapIntercept;

    /**
     * Métodos declarados explicitamente para serem interceptados, os outros não
     * serão.
     */
    private final List<Method> interceptedMethods;

    /**
     * Classe da interface implementada pela classe interceptada.
     */
    private final List<Class<?>> ifacesClass;

    /**
     * @param interceptor Iterceptador.
     * @param intercepted Interceptado.
     * @param ifaceClass Interface da classe interceptada.
     * @param methods Métodos a serem interceptados. Todos serão considerados se
     * nenhum for informado.
     */
    private InterceptorInvocationHandler(final Interceptor interceptor,
        final Class<?> ifaceClass, final Object intercepted,
        final Method... methods) {
        this.interceptor = notNull(interceptor, "Interceptor cannot be null.");
        this.intercepted = notNull(intercepted, "Intercepted cannot be null.");
        //TODO verificar se métodos são interceptáveis de fato
        this.interceptedMethods = Arrays.asList(methods);

        this.mapIntercept = new HashMap<Method, Boolean>(intercepted.getClass()
                                                                    .getDeclaredMethods().length);
        this.ifacesClass = Reflections.getInterfaces(ifaceClass);
        this.ifacesClass.add(ifaceClass);
    }

    /**
     * Verifica se método deve ser interceptado.
     *
     * @param method Metódo a ser interceptado.
     * @return "true" para interceptar.
     */
    private boolean canIntercept(final Method method) {
        // método em cache, regra já é conhecida
        if (mapIntercept.containsKey(method)) {
            return mapIntercept.get(method);
        }

        if (!interceptedMethods.isEmpty()) {
            if (!interceptedMethods.contains(method)) {
                // regra: se métodos a serem interceptados forem explicitamente
                // informados, somente eles serão interceptados
                mapIntercept.put(method, false);

                return false;
            }
        } else {
            // regra: métodos não declarados na interface e em suas superiores 
            // não são interceptados
            boolean isDeclared = false;

            for (Class<?> classs : ifacesClass) {
                isDeclared |= Reflections.isDeclared(classs, method);
            }

            if (!isDeclared) {
                mapIntercept.put(method, false);

                return false;
            }
        }

        mapIntercept.put(method, true);

        return true;
    }

    @Override
    public Object invoke(final Object proxy, final Method method,
        final Object[] args) throws Throwable {
        if (!canIntercept(method)) {
            // método não deve ser interceptado
            return proceed(method, args);
        }

        Object result = null;

        try {
            interceptor.doBefore(method, args);

            result = proceed(method, args);
        } catch (Throwable t) {
            return interceptor.doOnError(t);
        } finally {
            interceptor.doAfter(result);
        }

        return result;
    }

    /**
     * Realiza a preparação do ambiente de interceptação e cria uma nova
     * instância do objeto já interceptado.
     *
     * @param interceptor Interceptador.
     * @param ifaceClass Interface da classe a ser interceptada. Deve ser uma
     * interface!
     * @param intercepted Classe a ser interceptada.
     * @param methods Métodos a serem interceptados. Todos serão considerados se
     * nenhum for informado.
     * @return Classe a interceptada.
     */
    public static <T, E extends T> T newInstance(
        final Interceptor interceptor, final Class<T> ifaceClass,
        final E intercepted, final Method... methods) {
        interfaceRequired(notNull(ifaceClass,
                getParamMessage(MSG_NOT_NULL, "ifaceClass")),
            getParamMessage(MSG_NOT_AN_INTERFACE, "ifaceClass"));

        InterceptorInvocationHandler handler = new InterceptorInvocationHandler(interceptor,
                ifaceClass, intercepted, methods);
        Class<?>[] instances = { ifaceClass };

        return Casts.objCast(ifaceClass,
            Proxy.newProxyInstance(ifaceClass.getClassLoader(), instances,
                handler), true);
    }

    /**
     * Invoca o método de fato.
     *
     * @param method Método a ser executado.
     * @param args Argumentos do método.
     * @return Resultado da execução.
     * @throws IllegalAccessException Se houver.
     * @throws InvocationTargetException  Se houver.
     */
    private Object proceed(final Method method, final Object[] args)
        throws IllegalAccessException, InvocationTargetException {
        return method.invoke(intercepted, args);
    }
}
